using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Metadata.Edm;
using System.Linq.Expressions;
using System.Data.Objects;
using Microsoft.Reporting.WebForms;
using System.Web;
using System.Transactions;
using System.Data.EntityClient;
using System.Data.Objects.DataClasses;
using System.Reflection;
using Q.Framework.Extension;
using System.Data.Linq.Mapping;
using System.Text;
using Q.Framework.WebCache;

namespace Q.Framework.Data
{
    /// <summary>
    /// Classe base para facilitar o acesso e alteração dos dados.
    /// </summary>
    /// <remarks>DAO = Data Access Object</remarks>
    /// <typeparam name="T">Classe Business</typeparam>
    /// <typeparam name="C">Classe que representa o Contexto</typeparam>
    public class DAO<T>
    {
        private static object syncRoot = new Object();
        private static ObjectContext dbContext;
        private ObjectContext DbContext
        {
            get
            {
                dbContext = ContextManager.Context;

                if (dbContext.Connection.State == ConnectionState.Closed)
                    dbContext.Connection.Open();

                return dbContext;
            }
        }

        private IDbTransaction _DbConnection;
        private IDbTransaction DbConnection
        {
            get { return _DbConnection; }
            set { _DbConnection = value; }
        }

        private T Update(T obj)
        {
            Consistency(obj);

            EntityKey key;
            Object originalItem;

            key = DbContext.CreateEntityKey(GetEntitySetName(), obj);

            if (DbContext.TryGetObjectByKey(key, out originalItem))
                DbContext.ApplyCurrentValues<Object>(GetEntitySetName(), obj);

            DbContext.SaveChanges();

            return obj;
        }

        private T Insert(T obj)
        {
            Consistency(obj);

            DbContext.AddObject(GetEntitySetName(), obj);

            DbContext.SaveChanges();

            return obj;
        }

        /// <summary>
        /// Salva o objeto selecionado
        /// </summary>
        /// <param name="obj">Objeto à ser salvo</param>
        /// <returns>Retorna o objeto salvo</returns>
        public virtual T Save(T obj)
        {
            try
            {
                using (TransactionScope scope = new TransactionScope())
                {
                    EntityKey key = DbContext.CreateEntityKey(GetEntitySetName(), obj);

                    BeforeSave(obj);

                    String validations = String.Empty;

                    validations += ValidateMaxLength(obj);

                    validations += ValidateRequiredFields(obj);

                    if (!validations.IsNullOrWhiteSpace())
                    {
                        StringBuilder sbValidation = new StringBuilder(String.Format("Events to Save an object of type '{0}':", typeof(T).Name));
                        sbValidation.AppendLine(validations);
                        throw new Exception(sbValidation.ToString());
                    }

                    if (Convert.ToInt32(key.EntityKeyValues[0].Value) == 0)
                        Insert(obj);
                    else
                        Update(obj);

                    AfterSave(obj);

                    scope.Complete();

                    return obj;
                }

            }
            catch (Exception)
            {
                throw;
            }
        }

        public virtual void AfterSave(T obj) { }

        public virtual void BeforeSave(T obj) { }

        private string ValidateRequiredFields(T obj)
        {
            PropertyInfo[] properties = typeof(T).GetProperties();
            List<PropertyInfo> mandatoryProperties = new List<PropertyInfo>();
            foreach (PropertyInfo pI in properties)
            {
                System.Object[] attributes = pI.GetCustomAttributes(true);
                foreach (object attribute in attributes)
                    if ((attribute is Required) && pI.GetValue(obj, null) == null)
                        mandatoryProperties.Add(pI);
            }

            String result = String.Empty;
            if (mandatoryProperties.Count > 0)
            {
                StringBuilder sbValidation = new StringBuilder();
                sbValidation.AppendLine();
                foreach (var item in mandatoryProperties)
                    sbValidation.AppendLine(String.Format(Properties.Resources.MandatoryPropertyValidationMessage, item.Name));

                result = sbValidation.ToString();
            }

            return result;
        }

        private String ValidateMaxLength(T obj)
        {
            PropertyInfo[] properties = typeof(T).GetProperties();
            List<KeyValuePair<PropertyInfo, Int32>> truncatedProperties = new List<KeyValuePair<PropertyInfo, Int32>>();
            foreach (PropertyInfo pI in properties)
            {
                System.Object[] attributes = pI.GetCustomAttributes(true);
                foreach (object attribute in attributes)
                    if ((attribute is MaxLength) && (pI.GetValue(obj, null) as String) != null && (pI.GetValue(obj, null) as String).Length > ((MaxLength)attribute).Value)
                        truncatedProperties.Add(new KeyValuePair<PropertyInfo, Int32>(pI, ((MaxLength)attribute).Value));
            }

            String result = String.Empty;
            if (truncatedProperties.Count > 0)
            {
                StringBuilder sbValidation = new StringBuilder();
                sbValidation.AppendLine();
                foreach (var item in truncatedProperties)
                    sbValidation.AppendLine(String.Format(Properties.Resources.MaxLengthValidationMessage, item.Key.Name, item.Value, typeof(T).GetProperty(item.Key.Name).GetValue(obj, null).ToString().Length));

                result = sbValidation.ToString();
            }

            return result;
        }

        /// <summary>
        /// Exclui o objeto especificado
        /// </summary>
        /// <param name="obj">Objeto a ser excluido</param>
        public virtual void Delete(T obj)
        {
            try
            {
                using (TransactionScope scope = new TransactionScope())
                {
                    EntityKey key = DbContext.CreateEntityKey(GetEntitySetName(), obj);

                    if (Convert.ToInt32(key.EntityKeyValues.First().Value) != 0)
                    {
                        ConsistencyToDelete(obj);

                        ObjectQuery<T> objQuery = new ObjectQuery<T>(String.Format("SELECT VALUE {0} FROM {0} WHERE {0}.ID = @ID", GetEntitySetName()), DbContext);

                        objQuery.Parameters.Add(new ObjectParameter("ID", Convert.ToInt32(key.EntityKeyValues.First().Value)));

                        var del = objQuery.FirstOrDefault();

                        DbContext.DeleteObject(del);
                        DbContext.SaveChanges();
                    }

                    scope.Complete();
                }

            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// Seleciona o primeiro registro que corresponda a condição filtrada
        /// </summary>
        /// <param name="where">Condição a ser filtrada</param>
        /// <returns>Primeiro registro do Tipo T encontrado. Se não encontrar nenhum retorna nulo</returns>
        public T SelectOne(Expression<Func<T, Boolean>> where)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.SelectOne<T>(where);
            IQueryable<T> query = DbContext.CreateQuery<T>(GetEntitySetName()).Where(where);

            return query.FirstOrDefault();
        }

        /// <summary>
        /// Retorna o objeto pelo ID (PrimaryKey)
        /// </summary>
        /// <param name="ID">ID buscado</param>
        /// <returns>Objeto do Tipo T</returns>
        public T GetById(Int32 ID)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.GetById<T>(ID);
            ObjectQuery<T> objQuery =
        new ObjectQuery<T>(String.Format("SELECT VALUE {0} FROM {0} WHERE {0}.{1} = @ID", GetEntitySetName(), GetPrimaryKeyInfo().Name), DbContext);

            objQuery.Parameters.Add(new ObjectParameter("ID", ID));

            return objQuery.FirstOrDefault();
        }


        /// <summary>
        /// Busca registros utilizando a classe search para filtragem
        /// </summary>
        /// <typeparam name="E">Objeto do Tipo IQueryableObject&lt;T&gt;</typeparam>
        /// <param name="criteria">Critério de Busca</param>
        /// <returns>Lista com os objetos encontrados</returns>
        public List<T> Search<E>(E criteria) where E : IQueryableObject<T>
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.Search<T, E>(criteria);

            if (criteria != null)
                return criteria.GetQuery(GetQueryable()).ToList();
            return (List<T>)SelectAll();
        }

        /// <summary>
        /// Método que retorna uma lista com função de paginação
        /// </summary>
        /// <param name="order">Propriedade para ordenação</param>
        /// <param name="pageIndex">Número da página atual</param>
        /// <param name="pageSize">Quantidade de Registros por página</param>
        /// <returns>Objeto do Tipo PagedList</returns>
        public PagedList<T> GetPagedList(String order, int pageIndex, int pageSize)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.GetPagedList<T>(order, pageIndex, pageSize);
            string sort = SortExpression(order, null);
            return new PagedList<T>(GetQueryable().OrderBy(sort), pageIndex, pageSize);
        }

        /// <summary>
        /// Método que retorna uma lista com função de paginação
        /// </summary>
        /// <param name="order">Propriedade para ordenação</param>
        /// <param name="pageIndex">Número da página atual</param>
        /// <param name="pageSize">Quantidade de Registros por página</param>
        /// <returns>Objeto do Tipo PagedList</returns>
        public PagedList<T> GetPagedList(String order, String where, int pageIndex, int pageSize)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.GetPagedList<T>(order, where, pageIndex, pageSize);
            string sort = SortExpression(order, null);

            return new PagedList<T>(GetQueryable().OrderBy(sort), pageIndex, pageSize);
        }

        /// <summary>
        /// Método que retorna uma lista com função de paginação
        /// </summary>
        /// <param name="order">Propriedade para ordenação</param>
        /// <param name="where">Condição de Filtragem</param>
        /// <param name="pageIndex">Número da página atual</param>
        /// <param name="pageSize">Quantidade de Registros por página</param>
        /// <returns>Objeto do Tipo PagedList</returns>
        public PagedList<T> GetPagedList(String order, Expression<Func<T, Boolean>> where, int pageIndex, int pageSize)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.GetPagedList<T>(order, where, pageIndex, pageSize);
            string sort = SortExpression(order, null);
            return new PagedList<T>(GetQueryable().Where(where).OrderBy(sort), pageIndex, pageSize);
        }

        /// <summary>
        /// Método que retorna uma lista com função de paginação baseado em uma classe de critério de Busca
        /// </summary>
        /// <typeparam name="E">Classe que herda de IQueryableObject</typeparam>
        /// <param name="order">Propriedade para ordenação</param>
        /// <param name="criteria">Condição de Filtragem</param>
        /// <param name="pageIndex">Número da página atual</param>
        /// <param name="pageSize">Quantidade de Registros por página</param>
        /// <returns>Objeto do Tipo PagedList</returns>
        public PagedList<T> GetPagedList(String order, IQueryableObject<T> criteria, int pageIndex, int pageSize)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.GetPagedList<T>(order, criteria, pageIndex, pageSize);
            string sort = SortExpression(order, null);
            return new PagedList<T>(criteria.GetQuery(GetQueryable()).OrderBy(sort), pageIndex, pageSize);
        }

        /// <summary>
        /// Obtém um objeto do tipo IQueryable
        /// </summary>
        /// <returns>Objeto IQueryable</returns>
        public IQueryable<T> GetQueryable()
        {
            return DbContext.CreateQuery<T>(GetEntitySetName());
        }

        /// <summary>
        /// Obtem todos os registros
        /// </summary>
        /// <param name="maximumRows">Número Máximo de Registros</param>
        /// <param name="startRowIndex">Indice do primeiros registro</param>
        /// <returns>Lista de Objetos do tipo T</returns>
        public List<T> SelectAll(Int32 maximumRows, Int32 startRowIndex)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.SelectAll<T>(maximumRows, startRowIndex);
            return (List<T>)DbContext.CreateQuery<T>(GetEntitySetName()).Skip<T>(startRowIndex).Take(maximumRows).ToList();
        }

        /// <summary>
        /// Obtem todos os registros
        /// </summary>
        /// <returns>Lista de Objetos do tipo T</returns>
        public List<T> SelectAll(Boolean cacheOrigin = false)
        {
            if (!cacheOrigin && HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.SelectAll<T>();
            return (List<T>)DbContext.CreateQuery<T>(GetEntitySetName()).ToList();
        }


        /// <summary>
        /// Obtém todos os registros que satisfaçam a condição passada
        /// </summary>
        /// <param name="where">Condição de Filtragem</param>
        /// <returns>Lista de Objetos do tipo T</returns>
        public List<T> SelectWhere(Expression<Func<T, Boolean>> where)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.SelectWhere<T>(where);
            return (List<T>)DbContext.CreateQuery<T>(GetEntitySetName()).Where(where).ToList();
        }

        /// <summary>
        /// Obtém todos os registros que satisfaçam a condição passada
        /// </summary>
        /// <param name="where">Condição de Filtragem</param>
        /// <param name="maximumRows">Número Máximo de Registros</param>
        /// <param name="startRowIndex">Indice do primeiros registro</param>
        /// <returns>Lista de Objetos do tipo T</returns>
        public List<T> SelectWhere(Expression<Func<T, Boolean>> where, Int32 maximumRows, Int32 startRowIndex)
        {
            if (HttpContext.Current != null && CacheManager.CacheExists<T>())
                return CacheManager.SelectWhere<T>(where, maximumRows, startRowIndex);
            return (List<T>)DbContext.CreateQuery<T>(GetEntitySetName()).Where(where).Skip<T>(startRowIndex).Take(maximumRows).ToList();
        }

        public String GetEntitySetName()
        {
            return ContextManager.GetEntitySetName<T>();
        }

        /// <summary>
        /// Método que contém as consistências antes da Inserção/Atualização de um registro
        /// </summary>
        /// <param name="obj">Objeto à ser consistido</param>
        public virtual void Consistency(T obj) { }

        /// <summary>
        /// Método que contém as consistências antes da Exclusão de um registro
        /// </summary>
        /// <param name="obj">Objeto à ser consistido</param>
        public virtual void ConsistencyToDelete(T obj) { }

        /// <summary>
        /// Obtem o relatório padrão para classes que contém apenas ID e Descricao
        /// </summary>
        /// <param name="description">Descrição para filtragem</param>
        /// <param name="descriptionFieldName">Nome do Campo descrição na classe: (Padrao: Descricao)</param>
        /// <returns>Objeto de definição do relatório</returns>
        public ReportViewer ReportDefault(String description, String descriptionFieldName = "Descricao")
        {
            ObjectQuery<T> objQuery =
                new ObjectQuery<T>(String.Format("SELECT VALUE {0} FROM {0} WHERE {0}.{1} LIKE '%' + @Description + '%'", GetEntitySetName(), descriptionFieldName), DbContext);

            objQuery.Parameters.Add(new ObjectParameter("Description", description));

            ReportViewer Viewer = new ReportViewer();

            Viewer.LocalReport.DataSources.Add(new ReportDataSource("Padrao", objQuery.ToList()));
            Viewer.LocalReport.ReportPath = System.Web.HttpContext.Current.Server.MapPath(VirtualPathUtility.ToAbsolute("~/Relatorio/Padrao.rdlc"));

            return Viewer;
        }

        /// <summary>
        /// Verifica se existe algum registro na tabela que satisfaça a condição passada
        /// </summary>
        /// <param name="condition">Condição de Filtragem</param>
        /// <returns></returns>
        public Boolean Any(Expression<Func<T, Boolean>> condition = null)
        {
            IQueryable<T> query = GetQueryable();
            if (condition != null)
                query = query.Where(condition);
            return query.Skip(0).Take(1).FirstOrDefault() != null;
        }

        private static PropertyInfo GetPrimaryKeyInfo()
        {
            PropertyInfo[] properties = typeof(T).GetProperties();
            foreach (PropertyInfo pI in properties)
            {
                System.Object[] attributes = pI.GetCustomAttributes(true);
                foreach (object attribute in attributes)
                    if (attribute is PrimaryKey)
                        return pI;
            }
            return properties.FirstOrDefault(p => p.Name == "ID");
        }

        /// <summary>
        /// Retorna a expressão para ser usado por um IQueryable baseado numa propriedade passada como String 
        /// </summary>
        /// <param name="sortExpression">Nome da Propriedade para Ordenação</param>
        /// <param name="sortDirection">Ordenação ascendente ou Descendente(ASC/DESC)</param>
        /// <param name="defaultKey"></param>
        /// <returns></returns>
        public static string SortExpression(string sortExpression, string sortDirection, string defaultKey = null)
        {
            if (string.IsNullOrEmpty(sortExpression))
            {
                if (string.IsNullOrEmpty(defaultKey))
                {
                    // Default the sort expression to the primary key
                    var key = GetPrimaryKeyInfo();
                    sortExpression = key.Name;
                }
                else
                    sortExpression = defaultKey;
            }
            return string.Format("it.{0} {1}", sortExpression, sortDirection);
        }
    }

}